/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package biofilter.filters;

import biofilter.exceptions.InvalidRecordException;
import biofilter.records.OperonRecord;
import biofilter.records.Record;
import biofilter.records.ScoredRegionRecord;
import com.hp.hpl.jena.shared.AddDeniedException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This filter takes scored regions (both genes and gaps) and converts 
 * the scored regions into Operon Predictions.
 * 
 * When we say an operon, what we mean is a transcriptional unit.
 * This includes
 * 1) a set of genes
 * 2) a transcription start site
 * 3) a transcription stop site. 
 * 
 * @author Daniel Quest
 * @date   April 19 2010
 */
public class ScoredRegions2OperonFilter extends JoinSplitFilterBase {


    public ScoredRegions2OperonFilter(){

    }
    private String gapToken = "GAP";  //The gap token is the GFF name given to the regions filled.
    private String geneToken = "CDS"; //Token used to determine if the region is a gene/coding sequence.
    public ScoredRegions2OperonFilter(String gapToken, String geneToken){
        this.gapToken = gapToken;
        this.geneToken = geneToken;
    }

    public String getGapToken() {
        return gapToken;
    }

    public void setGapToken(String gapToken) {
        this.gapToken = gapToken;
    }

    public String getGeneToken() {
        return geneToken;
    }

    public void setGeneToken(String geneToken) {
        this.geneToken = geneToken;
    }

    /**
     * join attempts to merge two operons into one data structure based on the
     * following rules:
     * @param o1 - operon1
     * @param o2 - operon2
     * @return null if o1 and o2 could not be joined
     * else return a joined operon record.
     *
     * Note coverage:
     * _____________ : low/zero coverage
     * ------------- : medium coverage... could have a few zeros
     * ============= : high coverage ... absolutely no zeros.
     *
     * 1)
     *  ----->
     *     ------> : must merge, return o1 + o2;
     *
     * 2)
     * ----->  <------ : can not merge, return null;
     * 3)
     * ----->
     *     <------ : can not merge, return null;
     * 4)
     * <-----  ------> : can not merge, return null;
     * note:
     * <-----
     *     ------> : is not allowed by prodigal
     * 5)
     * ===============
     * ------>  ------> : Merge... return joint operon
     *
     * ===============
     * <------  <------ : Merge... return joint operon
     * 6)
     * -------__-------
     * ------>  ------> : Can not merge, return null;
     *
     * -------__------
     * <------  <------ : Can not merge, return null;
     */
    private OperonRecord join(OperonRecord o1, OperonRecord o2){
        //First, check that they are not acctually the same operon... if so, just return one of them
        if(o2.compareTo(o1) == 0){ return o1; }
        //Second ensure that o1 comes before o2 on the genome.
        if(o2.compareTo(o1) == -1){ //they are out of order, swap them.
            OperonRecord temp = o1;
            o1 = o2;
            o2 = temp;
        }
        /*
         * 1a)
         *  ----->
         *     ------> : must merge, return o1 + o2;
         * 1b)
         *  <-----
         *     <------ : must merge, return o1 + o2;
         */
        if(o1.getEnd() >= o2.getStart() && o1.getStrand() == o2.getStrand() ){
            return new OperonRecord(o1,o2);
        }
        /* 2)
         * ----->  <------ : can not merge, return null;
         * 3)
         * ----->
         *     <------ : can not merge, return null;
         * 4)
         * <-----  ------> : can not merge, return null;
         * note:
         * <-----
         *     ------> : is not allowed by prodigal
         */
        if(o1.getStrand() != o2.getStrand()){
            return null;
        }
        /*
        /* 5)
         * ===============
         * ------>  ------> : Merge... return joint operon
         *
         * ===============
         * <------  <------ : Merge... return joint operon
         */
        ScoredRegionRecord endgap = o1.maxGap();
        ScoredRegionRecord startgap = o2.minGap();
        if(endgap == null || startgap == null){ return null; } //if we don't have a shared gap, then they can not be merged!
        if( endgap.compareTo(startgap) == 0 && o1.getStrand() == o2.getStrand() ){
            if(endgap.getMinOverRegion() > 0){ //continuous coverage
                return new OperonRecord(o1,o2);
            }else{
        /* 6)
         * -------__-------
         * ------>  ------> : Can not merge, return null;
         *
         * -------__------
         * <------  <------ : Can not merge, return null;
         */
                return null;
            }
        }
        return null;  //probably becaus no coverage links the pairs.
    }

    /**
     * capGaps():
     */
    private OperonRecord capGaps(LinkedList<ScoredRegionRecord> buffer, OperonRecord operon){
        if(operon == null) return null;
        int pos = Integer.MIN_VALUE;
        ScoredRegionRecord gene = null;
        //first, find the gene in the operon, there should only be one. (infact, at this stage in the process there should only be one feature... mainly the gene with enough coverage
        for(int i=0;i<operon.size();i++){
            ScoredRegionRecord tmp = operon.getPos(i);
            if(tmp.isType(geneToken)){
                gene = tmp;
            }
        }
        if(gene == null) return null;
        //now find the position of that gene in the buffer.
        for(int i=buffer.size()-1;i>=0;i--){
            ScoredRegionRecord tmp = buffer.get(i);
            if(tmp.compareTo(gene) == 0){
                pos = i;
            }
        }
        if(pos == Integer.MIN_VALUE){ return null; }//gene not found in buffer.
        //can I place a gap on the front of the operon?
        if(pos > 0){
            ScoredRegionRecord tmp = buffer.get(pos -1);
            if(tmp!=null && tmp.isType(gapToken)){
                operon.add(tmp); //don't have to worry if this is a duplicate
            }
        }
        //can I place a gap on the end of an operon?
        if(pos < (buffer.size() -1)){
            ScoredRegionRecord tmp = buffer.get(pos + 1);
            if( tmp!=null && tmp.isType(gapToken) ){
                operon.add(tmp);  //don't have to worry if this is a duplicate
            }

        }
        return operon;
    }

    private LinkedList<OperonRecord> mergeOperons(int i, int j, LinkedList<OperonRecord> olist){
        if(i>=j) return olist;
        if( i == olist.size()-1 || j == olist.size()-1 ){ return olist; }
        OperonRecord o2 = olist.remove(j);
        OperonRecord o1 = olist.remove(i);
        //can we join i and j?
        OperonRecord join = join(o1,o2);
        if(join == null){ //can't join the two, move on to the next pair
            olist.add(i, o1);
            olist.add(j, o2);
            i=j;
            j++;
            return mergeOperons(i,j,olist);
        }else { //they merged!
            olist.add(i, join);
            return mergeOperons(i,j,olist);
        }
    }

    @Override
    protected Record transformRecord(Record r) throws InvalidRecordException {     
        Logger.getLogger(this.getClass().toString()).log(Level.INFO, "Transforming Scored Regions to Operons. ");
        LinkedList<OperonRecord> operons = new LinkedList<OperonRecord>();
        LinkedList<ScoredRegionRecord> buffer = new LinkedList<ScoredRegionRecord>();
        Iterator i = this.inputQueue.iterator();
        int count=0;
        //first, go off and make every gene with enough data an operon.
        while(i.hasNext()){
            count++;
            ScoredRegionRecord srr = (ScoredRegionRecord) i.next();
            //System.err.println(srr.get());
            buffer.add(srr);
            //if the scored region is a gap, add it to the end of the last operon in the list.
                 //if(srr.isType(this.gapToken) && operons.size() > 0){
                 //do nothing... we will add this gap in capGaps...
            //if the scored region is a gene, make a new operon and add it to the list
            if(srr.isType(this.geneToken)){                
                //does the region have enough coverage to continue?
                if(srr.flagedAsLowCoverage() == false){
                    OperonRecord o = new OperonRecord();
                    o.add(srr);
                    operons.add(o);                 
                }//else ignore it as it is low coverage...
            }
            if(operons.size() >1){
                //We need to add the gap to both the previous operon and the newly created one
                OperonRecord last = operons.removeLast();
                OperonRecord secondLast = operons.removeLast();
                //tie up the previous operon by adding any final gap.
                if(secondLast != null){
                    OperonRecord alt = this.capGaps(buffer, secondLast);
                    if(alt != null){
                        operons.add(alt);
                    }else {
                        operons.add(secondLast);
                    }
                }
                if(last != null){
                    OperonRecord alt = this.capGaps(buffer, last);
                    if(alt != null){
                        operons.add(alt);
                    }else {
                        operons.add(last);
                    }
                }
            }
        }//end while

        //print
        //Iterator iops = operons.iterator();
        //while(iops.hasNext()){
        //    OperonRecord next = (OperonRecord) iops.next();
        //    System.err.println(next.get());
        //}
        //now go back and merge as many single gene operons as possible.
        if(operons.size() > 1){
            operons = mergeOperons(0, 1, operons);
        }

        //System.err.println("\n*******AFTER MERGE*********");
        //iops = operons.iterator();
        //while(iops.hasNext()){
        //    OperonRecord next = (OperonRecord) iops.next();
        //    System.err.println(next.get());
        //}

        //Collections.sort(anno); ~~ or the other order if we want
        Iterator iops2 = operons.iterator();
        while(iops2.hasNext()){
            this.outputQueue.add( (OperonRecord) iops2.next() );
            //add the known feature to the output queue
        }
        return (Record) this.outputQueue.removeFirst();

    }

    public Record peek() throws InvalidRecordException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

}
